"""
一个简单的内部系统，用户提交修改默认值。
实现了不允许在短时间内多次提交和保存操作日志的功能。
日期：2019/9/25
"""

# 切记，不要随便使用from * import *这种包导入格式，会引发不可预知的错误！！哈哈！！
from requests import Session
from openpyxl import load_workbook
from flask import Flask
from flask import request
import json
import datetime

data = {'username': 'admin', 'password': 'abc'}
html = """
    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
    <meta charset="gb2312" />
    <style>
    body{text-align:center}
    </style>
    </head>
    <body>
    <form method="POST">
    输入ID:
    <input type="text" name="id" value="">
    <input type="submit" value="提交">
    <br>?
    </form>
    </body>
    </html>
"""
app = Flask(__name__)


class Sheet:
    """
    简单对Excel表格操作进行封装。
    """
    def __init__(self, _read_path):
        self.lwb = load_workbook(filename=_read_path)
        self.read_sheet = None

    def set_read_sheet(self, name):
        """ 设置指定的工作表。"""
        self.read_sheet = self.lwb[name]

    def read(self, xy):
        """ 从指定的行列坐标中获取数据。"""
        return self.read_sheet[xy].value


class ACC:
    """ 模拟登录，提交并修改数据。"""
    def __init__(self, s_id, s_pw, group_id):
        post_url = 'http://172.17.1.31:9080/api/signin'
        self.get_url_search = 'http://172.17.1.31:9080/api/user/list?page=1&size=30&group_id={}&name='.format(group_id)
        self.post_url_change = 'http://172.17.1.31:9080/api/user/update/'
        self.sessions = Session()
        _post = self.sessions.post(post_url, data)
        token = json.loads(_post.text)['data']['token']
        self.student_id = s_id
        self.student_password = s_pw
        self.headers = {'Authorization': 'Bearer ' + token}
        self.params = {'group_id': group_id, 'name': s_id, 'password': s_pw, 'descr': ''}

    def search_id(self):
        """ 通过搜索ID获得修改对应ID的编号。"""
        _get_url_search = self.get_url_search + self.student_id
        r = self.sessions.get(_get_url_search, headers=self.headers)
        d = json.loads(r.text)
        if isinstance(d, dict):
            data_list = d.get('data').get('data')
            if isinstance(data_list, list):
                for each_dict in data_list:
                    if each_dict['name'] == self.student_id:
                        return each_dict['id']
                else:
                    return None
            return None

    def change_pwd(self, id_api):
        """ 提交数据到系统修改默认值。
        :return: 返回值用于判断修改结果成功与否。
        """
        id_api = str(id_api)
        if id_api:
            r = self.sessions.post(self.post_url_change + id_api, headers=self.headers, params=self.params)
            rr = json.loads(r.text)
            if rr['success'] and rr['data']:
                return True
            else:
                return False


class ACCSheet:
    """ 从excel表格中获取默认密码。"""
    def __init__(self):
        path = r'd:\sheet\id.xlsx'
        self.sheet = Sheet(path)
        self.sheet.set_read_sheet('Sheet1')
        self.sheet_cache = {}
        self._load_cache()

    def _load_cache(self):
        """ 这是一个表格的缓存。只在第一次被加载，之后都不用再从Excel表格中取数据。"""
        for i in range(2, 20000):
            a = str(self.sheet.read('A' + str(i)))
            b = str(self.sheet.read('B' + str(i)))
            c = str(self.sheet.read('C' + str(i)))
            if a and b:
                self.sheet_cache[a] = [b, c]
            elif a is None or b is None:
                break

    def search_id_from_sheet(self, s_id):
        """ 从缓存表格中获取对应值。"""
        r = self.sheet_cache.get(s_id)
        if r:
            return r
        else:
            return None


class Verify:
    """ 主要用于整个流程的操作。
    流程：
        1. 从web页面获取ID。
        2. 从Excel表格中找出ID对应的默认值。
        3. 模拟平台登录，把ID和默认值提交到平台上进行数据修改。
    """
    def __init__(self):
        self._acc_sheet = ACCSheet()
        self.cache = {}         # 重置过的用户缓存。包括：ip，时间，修改id，修改次数，时间戳。
        self.logs = []          # 在verify方法中每一步返回的数据保存到日志中，最后写入日志文件。
        self.acc = None
        log_path = 'd:\\modify.log'     # 输出的日志文件路径。
        self.log_file = open(log_path, 'a')
        self.max_time = 15      # 最大时间内不允许多次提交，单位秒。
        self.times_in_max = 2   # 最大时间内不允许提交的次数。

    def verify(self, remote_ip, login_times, user_id):
        """
        整个操作流程的走向在这里写。
        :param remote_ip: 登录用户的IP。
        :param login_times: 登录时的时间。
        :param user_id: 提交的ID。
        :return: 返回修改结果。
        """
        self.logs.append(str(login_times).split('.')[0])
        self.logs.append(remote_ip)
        self.logs.append(user_id)

        result = self._user_input_verify(remote_ip, login_times, user_id)
        if result:
            self.logs.append(result)
            return result
        self._record_login_message(remote_ip, login_times, user_id)
        pwd_and_job = self._acc_sheet.search_id_from_sheet(user_id)
        if not pwd_and_job:
            self.logs.append('没有搜索到你要找的内容 ...')
            return '没有搜索到你要找的内容 ...'
        job = '15' if '学生' == pwd_and_job[1] else '16'
        self.acc = ACC(user_id, pwd_and_job[0], job)
        update_id = self.acc.search_id()
        if update_id is None:
            self.logs.append('未查找到数据 ...')
            return '未查找到数据 ...'
        if not self.acc.change_pwd(update_id):
            self.logs.append('修改过程中失败 ...')
            return '修改过程中失败 ...'
        else:
            self.logs.append('修改成功 ！！！')
            return '修改成功 ！！！'

    def save_log(self):
        """ 每次执行完之后需要保存操作日志。"""
        for _ in self.logs:
            self.log_file.write(_ + ', ')
        self.logs = []
        self.log_file.write('\n')
        self.log_file.flush()

    def _user_input_verify(self, remote_ip: str, login_times, user_id: str):
        """ 对用户输入的内容做过滤。
        :return: 该方法有返回值说明用户操作有误。
        """
        ip = self.cache.get(remote_ip)
        # 验证用户输入的内容。
        if not user_id.isalnum():
            return '确认你输入的内容 ...'
        # 10秒内请求次数超过3次并且上次登陆时间和这次的时间差小于30秒就拒绝修改操作。
        if ip and ip['request_times'] >= self.times_in_max and (login_times - ip['login_times']).seconds < self.max_time:
            return '不允许在短时间内多此提交 ...'

    def _record_login_message(self, remote_ip, login_time, user_id):
        """ 对用户登录的信息进行记录。"""
        ip = self.cache.get(remote_ip)
        if not ip:
            self.cache[remote_ip] = {'login_times': login_time, 'user_id': set(user_id), 'request_times': 1}
        else:
            ip.get('user_id').add(user_id)
            if ip['request_times'] <= self.times_in_max:
                ip['request_times'] += 1
            elif ip['request_times'] > self.times_in_max and (login_time - ip['login_times']).seconds > self.max_time:
                ip['login_times'] = login_time
                ip['request_times'] = 1


v = Verify()
"""
用于Flask页面显示。
"""
@app.route("/", methods=("GET", "POST"))
def login():
    remote_ip = request.remote_addr
    login_time = datetime.datetime.now()

    if request.method == 'GET':
        return html.replace('?', '')
    if request.method == 'POST':
        user_info = request.form.to_dict()
        user_id = user_info.get('id')
        result = v.verify(remote_ip, login_time, user_id)
        v.save_log()
        return html.replace('?', result)


if __name__ == '__main__':
    app.run(host='10.60.94.21', port=80, debug=False)
